﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace Effects.Wave
{
    /// <summary>
    ///  Class for writing Wave to the file
    /// </summary>
    public class WaveWriter
    {
        #region path
        // path to the output file

        /// <summary>
        ///  Path to the file
        /// </summary>
        protected string fileName;

        /// <summary>
        /// Gets or sets 
        /// </summary>
        private string FileName
        {
            get { return fileName; }
            set { fileName = value; }
        }

        #endregion

        #region output_file_stream
        // field for output file and or stream

        /// <summary>
        ///  BinaryWriter over the file specified by fileName
        /// </summary>
        protected BinaryWriter file;

        /// <summary>
        ///  Stream to write the Wave to.
        /// </summary>
        protected Stream stream;

        #endregion

        #region wave
        // wave to be write to the output

        /// <summary>
        ///  Instance of Wave to be written.
        /// </summary>
        protected Wave wave;
        /// <summary>
        /// Gets or sets Wave to be written to the file.
        /// </summary>
        public Wave Wave
        {
            get { return wave; }
            set { wave = value; }
        }

        #endregion

        #region constructor
        // constructors

        /// <summary>
        ///  Creates WaveWriter that will be writing to the file specified by fileName.
        /// </summary>
        /// <param name="fileName"> path to the file </param>
        public WaveWriter(string fileName)
        {
            this.fileName = fileName;
            this.stream = new FileStream(fileName, FileMode.Create, FileAccess.Write);
        }

        /// <summary>
        ///  Creates WaveWriter that will be writing to the given stream
        /// </summary>
        /// <param name="stream"> Stream to write to </param>
        public WaveWriter(Stream stream)
        {
            this.stream = stream;
        }

        #endregion

        #region write_method
        // writes given file to the stream

        /// <summary>
        ///  Occurs while performing write method to report changes in operation's progress.
        /// </summary>
        public event IoOperationReportProgressEventHandler ReportProgress;

        /// <summary>
        ///  Writes wave.
        /// </summary>
        public void Write()
        {
            try
            {

                //file = new BinaryWriter(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write), Encoding.Unicode);
                file = new BinaryWriter(stream, Encoding.Unicode);
                // writing header
                byte[] buffer = Encoding.ASCII.GetBytes("RIFF");
                file.Write(buffer);

                buffer = BitConverter.GetBytes(wave.Lenght);
                if (!BitConverter.IsLittleEndian)
                    Array.Reverse(buffer);
                file.Write(buffer);

                buffer = Encoding.ASCII.GetBytes("WAVE");
                file.Write(buffer);

                // writing format chunk
                buffer = wave.FmtChunk.ChunkToWrite();
                file.Write(buffer);

                // writing data chunk
                //buffer = wave.DataChunk.ChunkToWrite();
                buffer = ToByte(wave.DataChunk.Data,(short) wave.FmtChunk.BitsPerSample);
                
                // write id
                byte[] idBytes = Encoding.ASCII.GetBytes("data");
                file.Write(idBytes);

                byte[] lengthBytes = BitConverter.GetBytes(Convert.ToUInt32(buffer.Length));
                if (!BitConverter.IsLittleEndian)
                    Array.Reverse(lengthBytes);

                file.Write(lengthBytes);

                int tenPercentLength = buffer.Length / 10;
                for (int i = 0; i < 9; i++)
                {
                    file.Write(buffer, i * tenPercentLength, tenPercentLength);
                    IoOperationProgressEventArgs progressArgs = new IoOperationProgressEventArgs(i * 10);
                    if (this.ReportProgress != null)
                        ReportProgress(this, progressArgs);
                }

                // write remaining
                file.Write(buffer, 9 * tenPercentLength, buffer.Length % 10);
                IoOperationProgressEventArgs progressArg = new IoOperationProgressEventArgs(100);
                if (this.ReportProgress != null)
                    ReportProgress(this, progressArg);

            }
            catch (DirectoryNotFoundException e)
            {
                //Console.WriteLine("An error occured while trying to write file: " + fileName);
                throw new Exception("An error occured while trying to write file: " + fileName + "\nCheck the path", e);
            }
            catch (IOException e)
            {
                //Console.WriteLine("An error occured while trying to write file: " + fileName);
                throw new IOException("An error occured while trying to write file: " + fileName, e);
            }
            catch (Exception e)
            {
                // error occured while writing file
                throw new Exception(e.Message, e);
            }
            finally
            {
                if (file != null)
                    file.Close();
            }
        }

        #endregion

        #region convert_from_doubles
        /* Converting from samples as doubles from [-1;1] interval to raw byte data in little endian */

        /// <summary>
        ///  Converts double samples from [-1;1] interval to raw byte data in little endian.
        /// </summary>
        /// <param name="data"> Samples as doubles. </param>
        /// <param name="bitDepth"> Bit depth of the data. </param>
        /// <returns> Raw byte data in Little endian. </returns>
        private byte[] ToByte(double[] data, short bitDepth)
        {
            if (bitDepth == 8)
                return ToByte8bit(data);

            if (bitDepth == 16)
                return ToByte16bit(data);

            if (bitDepth == 24)
                return ToByte24bit(data);

            if (bitDepth == 32)
                return ToByte32bit(data);

            return null;
        }

        /// <summary>
        ///  Converts double samples from [-1;1] interval to raw byte data in little endian using bit depth 8.
        /// </summary>
        /// <param name="data"> Samples as doubles. </param>
        /// <returns> Raw byte data in Little endian. </returns>
        private byte[] ToByte8bit(double[] data)
        {
            byte[] ret = new byte[data.Length];
            for (int i = 0; i < data.Length; i++)
                ret[i] = Convert.ToByte(128 * data[i] + 127);

            return ret;
        }

        /// <summary>
        ///  Converts double samples from [-1;1] interval to raw byte data in little endian using bit depth 8.
        /// </summary>
        /// <param name="data"> Samples as doubles. </param>
        /// <returns> Raw byte data in Little endian. </returns>
        private byte[] ToByte16bit(double[] data)
        {
            byte[] ret = new byte[data.Length * 2];

            if (BitConverter.IsLittleEndian)
            {
                for (int i = 0, source_index = 0; i < data.Length; i++, source_index += 2)
                {
                    byte[] buffer = BitConverter.GetBytes(Convert.ToInt16(data[i] * 32767));

                    ret[source_index] = buffer[0];
                    ret[source_index + 1] = buffer[1];
                }
            }
            else
            {
                for (int i = 0, source_index = 0; i < data.Length; i++, source_index += 2)
                {
                    byte[] buffer = BitConverter.GetBytes(Convert.ToInt16(data[i] * 32767));

                    ret[source_index] = buffer[1];
                    ret[source_index + 1] = buffer[0];
                }
            }
            return ret;
        }

        /// <summary>
        ///  Converts double samples from [-1;1] interval to raw byte data in little endian using bit depth 8.
        /// </summary>
        /// <param name="data"> Samples as doubles. </param>
        /// <returns> Raw byte data in Little endian. </returns>
        private byte[] ToByte24bit(double[] data)
        {
            byte[] ret = new byte[data.Length * 3];

            if (BitConverter.IsLittleEndian)
            {
                for (int i = 0, source_index = 0; i < data.Length; i++, source_index += 3)
                {
                    byte[] buffer = BitConverter.GetBytes(Convert.ToInt32(data[i] * 8388607));

                    ret[source_index] = buffer[0];
                    ret[source_index + 1] = buffer[1];
                    ret[source_index + 2] = buffer[2];
                }
            }
            else
            {
                for (int i = 0, source_index = 0; i < data.Length; i++, source_index += 3)
                {
                    byte[] buffer = BitConverter.GetBytes(Convert.ToInt32(data[i] * 8388607));

                    ret[source_index] = buffer[2];
                    ret[source_index + 1] = buffer[1];
                    ret[source_index + 2] = buffer[0];
                }
            }
            return ret;
        }

        /// <summary>
        ///  Converts double samples from [-1;1] interval to raw byte data in little endian using bit depth 8.
        /// </summary>
        /// <param name="data"> Samples as doubles. </param>
        /// <returns> Raw byte data in Little endian. </returns>
        private byte[] ToByte32bit(double[] data)
        {
            byte[] ret = new byte[data.Length * 4];


            if (BitConverter.IsLittleEndian)
            {
                for (int i = 0, source_index = 0; i < data.Length; i++, source_index += 4)
                {
                    byte[] buffer = BitConverter.GetBytes(Convert.ToInt32(data[i] * 2147483647));

                    ret[source_index] = buffer[0];
                    ret[source_index + 1] = buffer[1];
                    ret[source_index + 2] = buffer[2];
                    ret[source_index + 3] = buffer[3];
                }
            }
            else
            {
                for (int i = 0, source_index = 0; i < data.Length; i++, source_index += 4)
                {
                    byte[] buffer = BitConverter.GetBytes(Convert.ToInt32(data[i] * 2147483647));

                    ret[source_index] = buffer[3];
                    ret[source_index + 1] = buffer[2];
                    ret[source_index + 2] = buffer[1];
                    ret[source_index + 3] = buffer[0];
                }

            }

            return ret;
        }

        #endregion
    }
}